TypeScript에서는 함수 타입을 다룰 대 중요한 개념인 **공변성(convariance)**과 반공변성(contravariance), 그리고 그것이 실무에서 자주 보게 되는 다음 두 형태의 타입 선언에 어떻게 적용되는지를 학습했습니다.
onChangeA?: (selected: T) => void;
onChangeB?(selected: T): void;
이 둘은 형식상으로는 동일한 의미를 갖지만, TypeScript의 타입 시스템은 이들을 다르게 해석할 수 있으며, 그 차이가 함수 타입의 변성 규칙에서 비롯된다는 점을 이해했습니다.
React같은 UI 프레임워크를 스다 보면, Props로 콜백 함수를 넘기는 경우가 매우 많습니다. 이때 함수의 매개변수나 반환값에 대해 어떤 타입을 선언하느냐에 따라 예상하지 못한 타입 에러가 발생하거나, 반대로 의도치 않은 타입이 허용되어 런타임에서 오류가 날 수 있는 위험한 상황이 생깁니다.
<Select
options={...}
onSelect={(value: T) => { ... }} // 이 콜백, 타입 안전할까?
>
() => A
는 () => B
의 서브타입(param: B) => void
는 (param: A) => void
의 서브타입아래 예제를 통해 더 자세히 알아볼 수 있습니다.
// 함수 타입을 속성으로 선언 (느슨한 체크)
type PropsA<T> = {
onChangeA?: (selected: T) => void;
};
// 함수 타입을 메서드 시그니처로 선언 (엄격한 체크)
type PropsB<T> = {
onChangeB?(selected: T): void;
};
이 둘은 선언 방식만 다를 뿐 의미적으로는 같지만, TypeScript는 엄격도에서 차이를 둡니다.
// 타입 구조
type PropsA<T> = {
onChangeA?: (selected: T) => void;
};
type PropsB<T> = {
onChangeB?(selected: T): void;
};
class Animal {
name = "animal";
}
class Dog extends Animal {
breed = "shiba";
}
const handleDog = (dog: Dog) => console.log(dog.breed);
// ✅ 느슨한 방식: PropsA (함수 타입 속성)
const a: PropsA<Animal> = {
onChangeA: handleDog // ✅ TypeScript는 이걸 기본적으로 허용
};
// ❌ 엄격한 방식: PropsB (메서드 시그니처)
const b: PropsB<Animal> = {
// ❌ strictFunctionTypes 켜져 있으면 오류 발생
onChangeB(dog: Dog) {
console.log(dog.breed);
}
};
strictFunctionTypes
이 차이가 발생하는 이유는 tsconfig.josn
의 strictFunctionTypes
설정 때문입니다.
{
"compilerOptions": {
"strict": true,
"strictFunctionTypes": true
}
}
✔️ 함수 타입의 매개변수에 대해 엄격하게 반공변성 체크를 적용합니다.
✔️ 그래서 PropsB
처럼 메서드 시그니처로 선언된 경우 Dog
→ Animal
대입은 거부됩니다.